use std::{
    collections::HashMap,
    fs::File,
    io::{BufRead, BufReader, Read},
    path::{Path, PathBuf},
};
use std::error::Error;
use md5::{Digest, Md5};
use zip::ZipArchive;

#[tauri::command]
fn greet(name: &str) -> String {
    println!("Received name from frontend: {}", name);
    format!("Hello, {}! You've been greeted from Rust!", name)
}

#[tauri::command(rename_all = "snake_case")]
fn compare_solutions(solution_path1: &str, solution_path2: &str) -> Result<String, String> {
    println!("Comparing solutions: {} and {}", solution_path1, solution_path2);
    
    let (temp_dir1, temp_dir2) = match unzip_solutions(solution_path1, solution_path2) {
        Ok(paths) => paths,
        Err(e) => return Err(format!("Failed to unzip solutions: {}", e)),
    };
    
    let modified_files = match compare_directories(&temp_dir1, &temp_dir2) {
        Ok(files) => files,
        Err(e) => return Err(format!("Comparison failed: {}", e)),
    };

    println!("these files changed: {:?}", &modified_files);
    
    // Clean up temporary directories
    /*
    if let Err(e) = std::fs::remove_dir_all(&temp_dir1) {
        eprintln!("Warning: Failed to clean up temp dir {}: {}", temp_dir1.display(), e);
    }
    if let Err(e) = std::fs::remove_dir_all(&temp_dir2) {
        eprintln!("Warning: Failed to clean up temp dir {}: {}", temp_dir2.display(), e);
    }
    */
    Ok(format!("Comparison complete. Found {} modified files.", modified_files.len()))
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .invoke_handler(tauri::generate_handler![greet, compare_solutions])
        .run(tauri::generate_context!())
        .expect("Failed to run Tauri application");
}

/// Represents a file with comparison metadata
#[derive(Debug)]
struct FileMetadata {
    path: PathBuf,
    md5: String,
    line_count: usize,
}

/// Represents a modified file to be sent to the frontend
#[derive(Debug)]
struct ModifiedFile {
    path: String,
    change_type: ChangeType,
    file_name: String,
    lines_changed: i32,
}

#[derive(Debug)]
enum ChangeType {
    Modified,
    AddedOrDeleted,
}

/// Unzips both solution files to temporary directories
fn unzip_solutions(path1: &str, path2: &str) -> Result<(PathBuf, PathBuf), Box<dyn Error>> {
    println!("Starting to unzip solutions...");
    
    // Verify input paths exist
    println!("Verifying input files exist...");
    if !Path::new(path1).exists() {
        return Err(format!("Solution file 1 does not exist: {}", path1).into());
    }
    if !Path::new(path2).exists() {
        return Err(format!("Solution file 2 does not exist: {}", path2).into());
    }

    let parent_dir = Path::new(path1).parent()
        .ok_or_else(|| format!("Invalid solution path: {}", path1))?;
    
    println!("Parent directory: {:?}", parent_dir);

    let temp_dir1 = parent_dir.join("solution1_temp");
    let temp_dir2 = parent_dir.join("solution2_temp");

    println!("Temporary directory 1: {:?}", temp_dir1);
    println!("Temporary directory 2: {:?}", temp_dir2);

    // Clean up existing directories if they exist
    println!("Cleaning up existing directories...");
    if temp_dir1.exists() {
        println!("Removing existing directory: {:?}", temp_dir1);
        std::fs::remove_dir_all(&temp_dir1).map_err(|e| {
            format!("Failed to remove directory {:?}: {}", temp_dir1, e)
        })?;
    }
    
    if temp_dir2.exists() {
        println!("Removing existing directory: {:?}", temp_dir2);
        std::fs::remove_dir_all(&temp_dir2).map_err(|e| {
            format!("Failed to remove directory {:?}: {}", temp_dir2, e)
        })?;
    }

    // Create new directories
    println!("Creating new directories...");
    std::fs::create_dir_all(&temp_dir1).map_err(|e| {
        format!("Failed to create directory {:?}: {}", temp_dir1, e)
    })?;
    println!("Successfully created directory: {:?}", temp_dir1);
    
    std::fs::create_dir_all(&temp_dir2).map_err(|e| {
        format!("Failed to create directory {:?}: {}", temp_dir2, e)
    })?;
    println!("Successfully created directory: {:?}", temp_dir2);

    // Extract zip files
    println!("Extracting solution 1...");
    extract_zip(path1, &temp_dir1).map_err(|e| {
        format!("Failed to extract {} to {:?}: {}", path1, temp_dir1, e)
    })?;
    println!("Successfully extracted solution 1");

    println!("Extracting solution 2...");
    extract_zip(path2, &temp_dir2).map_err(|e| {
        format!("Failed to extract {} to {:?}: {}", path2, temp_dir2, e)
    })?;
    println!("Successfully extracted solution 2");

    // Verify directories were created and have content
    println!("Verifying extraction results...");
    verify_directory(&temp_dir1)?;
    verify_directory(&temp_dir2)?;

    println!("Unzip completed successfully");
    Ok((temp_dir1, temp_dir2))
}

fn verify_directory(dir: &Path) -> Result<(), Box<dyn Error>> {
    if !dir.exists() {
        return Err(format!("Directory does not exist: {:?}", dir).into());
    }
    
    let entries = std::fs::read_dir(dir)
        .map_err(|e| format!("Failed to read directory {:?}: {}", dir, e))?;
    
    let entry_count = entries.count();
    println!("Directory {:?} contains {} items", dir, entry_count);
    
    if entry_count == 0 {
        eprintln!("Warning: Directory {:?} is empty", dir);
    }
    
    Ok(())
}

/// Extracts a zip file to the target directory
fn extract_zip(zip_path: &str, target_dir: &Path) -> zip::result::ZipResult<()> {
    let file = File::open(zip_path)?;
    let mut archive = ZipArchive::new(file)?;

    for i in 0..archive.len() {
        let mut file = archive.by_index(i)?;
        let outpath = target_dir.join(file.mangled_name());
        
        if file.name().ends_with('/') {
            std::fs::create_dir_all(&outpath)?;
        } else {
            if let Some(p) = outpath.parent() {
                std::fs::create_dir_all(p)?;
            }
            let mut outfile = File::create(&outpath)?;
            std::io::copy(&mut file, &mut outfile)?;
        }
    }
    Ok(())
}

/// Compares two directories recursively
fn compare_directories(dir1: &Path, dir2: &Path) -> Result<Vec<ModifiedFile>, Box<dyn Error>> {
    let files1 = scan_directory(dir1)?;
    let files2 = scan_directory(dir2)?;
    
    let mut modified_files = Vec::new();
    
    // Check for modified or deleted files in dir1
    for (rel_path, file1) in &files1 {
        match files2.get(rel_path) {
            Some(file2) if file1.md5 != file2.md5 => {
                let lines_changed = compute_line_differences(&file1.path, &file2.path)?;
                modified_files.push(ModifiedFile {
                    path: file1.path.display().to_string(),
                    change_type: ChangeType::Modified,
                    file_name: rel_path.clone(),
                    lines_changed: lines_changed as i32,
                });
            },
            None => {
                modified_files.push(ModifiedFile {
                    path: file1.path.display().to_string(),
                    change_type: ChangeType::AddedOrDeleted,
                    file_name: rel_path.clone(),
                    lines_changed: file1.line_count as i32,
                });
            },
            _ => {}
        }
    }
    
    // Check for new files in dir2 that weren't in dir1
    for (rel_path, file2) in &files2 {
        if !files1.contains_key(rel_path) {
            modified_files.push(ModifiedFile {
                path: file2.path.display().to_string(),
                change_type: ChangeType::AddedOrDeleted,
                file_name: rel_path.clone(),
                lines_changed: file2.line_count as i32,
            });
        }
    }
    
    Ok(modified_files)
}

/// Scans a directory recursively and collects file metadata
fn scan_directory(dir: &Path) -> Result<HashMap<String, FileMetadata>, Box<dyn Error>> {
    let mut files = HashMap::new();
    scan_directory_helper(dir, dir, &mut files)?;
    Ok(files)
}

fn scan_directory_helper(
    base_dir: &Path,
    current_dir: &Path,
    files: &mut HashMap<String, FileMetadata>,
) -> Result<(), Box<dyn Error>> {
    for entry in std::fs::read_dir(current_dir)? {
        let entry = entry?;
        let path = entry.path();
        
        if path.is_file() {
            let relative_path = path.strip_prefix(base_dir)?
                .to_str()
                .ok_or("Invalid UTF-8 path")?
                .replace('\\', "/"); // Normalize path separators
            
            let md5 = compute_md5(&path)?;
            let line_count = count_lines(&path)?;
            
            files.insert(relative_path, FileMetadata {
                path,
                md5,
                line_count,
            });
        } else if path.is_dir() {
            scan_directory_helper(base_dir, &path, files)?;
        }
    }
    Ok(())
}

/// Computes MD5 hash of a file
fn compute_md5(path: &Path) -> Result<String, Box<dyn Error>> {
    let mut file = File::open(path)?;
    let mut hasher = Md5::new();
    let mut buffer = [0; 8192]; // 8KB buffer
    
    loop {
        let bytes_read = file.read(&mut buffer)?;
        if bytes_read == 0 {
            break;
        }
        hasher.update(&buffer[..bytes_read]);
    }
    
    Ok(format!("{:x}", hasher.finalize()))
}

/// Counts lines in a file
fn count_lines(path: &Path) -> Result<usize, Box<dyn Error>> {
    let file = File::open(path)?;
    Ok(BufReader::new(file).lines().count())
}

/// Computes the number of differing lines between two files
fn compute_line_differences(path1: &Path, path2: &Path) -> Result<usize, Box<dyn Error>> {
    let file1 = File::open(path1)?;
    let file2 = File::open(path2)?;
    
    let lines1 = BufReader::new(file1).lines();
    let lines2 = BufReader::new(file2).lines();
    
    let mut diff_count = 0;
    
    for (line1, line2) in lines1.zip(lines2) {
        if line1? != line2? {
            diff_count += 1;
        }
    }
    
    // Account for files of different lengths
    let file1 = File::open(path1)?;
    let file2 = File::open(path2)?;
    let line_count1 = BufReader::new(file1).lines().count();
    let line_count2 = BufReader::new(file2).lines().count();
    
    Ok(diff_count + line_count1.abs_diff(line_count2))
}